iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0

多執行緒讓 CPU 可以同時多工工作,雖然可以提升 CPU 使用效率、減少資源浪費,但也潛藏著風險,其中最著名的為「資料競爭(data race)」。

註:可能很多文章會把資料競爭(data race)與競賽條件(race condition)混為一談,但兩者事實上並不相等,本篇先介紹「資料競爭」,待下一篇講完資料競爭的解決方案後,將發現這些方案尚不足以防範競賽條件,詳細於 Day 24 與 Day 25 文章中說明。

資料競爭形成條件

只要滿足下列條件,就能形成資料競爭:

  1. 複數個執行緒共用相同記憶體資源(如使用同一變數)。
  2. 至少有一個執行緒做的事情是「寫入」(如果都只是「讀取」不會有問題)。

以下來看個常見範例。

案例探討

在以下程式碼中,有兩個執行緒 t1t2 都要跑 addOneManyTimes() 函數,而這兩個執行緒:

  1. 共用 counter 全域變數。
  2. 都有「寫入」新資料到 counter

其中「寫入」新資料到 counter 這件事在 addOneManyTimes() 函數中進行,兩執行緒透過迴圈、各自把 counter 連續做十萬次 +1,最後以 cout 顯示運算完的結果:

#include <iostream>
#include <thread>

int counter = 0;

void addOneManyTimes() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;
    }
}

int main() {
    std::thread t1(addOneManyTimes);
    std::thread t2(addOneManyTimes);

    t1.join();
    t2.join();

    std::cout << "counter = " << counter << '\n';
    return 0;
}

兩個執行緒各加十萬次,所以最後的 counter 應該是 200000 對吧?但最終的結果並非如此,如在我的實驗中,第一次結果如下:

counter = 107479

第二次結果如下:

counter = 112398

第三次結果如下:

counter = 126925

每次結果都不一樣,但每次都不是預期的結果 200000,為什麼會這樣?

資料競爭原因探討

一個執行緒要改變 counter 變數的值,都會經過以下三步驟:

  1. counter 目前的值放到某暫存器。
  2. 在暫存器中把當下的值加 1。
  3. 把新值 counter 從暫存器寫回記憶體。

但現在有 t1t2 兩個執行緒,因此變成:

  1. t1 讀取到 counter = 0 並放到暫存器。
  2. t2 讀取到 counter = 0 並放到暫存器。
  3. t1 在暫存器將讀取到的 0 加 1,並將新 counter 值寫回記憶體。
  4. t2 在暫存器將讀取到的 0 加 1,並將新 counter 值寫回記憶體。

兩個執行緒都有 +1 沒錯,但兩者都是基於舊值運算,因此其中一次的加 1 就默默被吃掉了,這就是「資料競爭」。

參考資料

  1. Data Races in C++ (GeeksforGeeks)
  2. Data Races vs. Race Conditions: Understanding the Differences with Actors (Medium)

上一篇
Day 22-初探多執行緒
下一篇
Day 24-資料競爭的解決方法
系列文
韌體工程師的不只 0x10 個問題31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言